﻿#region License and Copyright Notice

//XOML: XML based object persistence framework

//Copyright (c) 2008 Ananth B.
//All rights reserved.

//The contents of this file are made available under the terms of the
//Eclipse Public License v1.0 (the "License") which accompanies this
//distribution, and is available at the following URL:
//http://www.opensource.org/licenses/eclipse-1.0.php

//Software distributed under the License is distributed on an "AS IS" basis,
//WITHOUT WARRANTY OF ANY KIND, either expressed or implied. See the License for
//the specific language governing rights and limitations under the License.

//By using this software in any fashion, you are agreeing to be bound by the
//terms of the License.

#endregion

using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;

namespace Xoml
{
    public sealed class StringTokenizer: IEnumerable<Token>, IEnumerable
    {
        const char EOF = (char)0;

        private int _line;
        private int _column;
        private int _pos;	// position within data
        private Token _previous;
        private Token _current;

        private string _data;

        private bool _ignoreWhiteSpace;
        private char[] _symbolChars;
        private char _quoteChar = '"';

        private int _saveLine;
        private int _saveCol;
        private int _savePos;

        private void Reset()
        {
            _ignoreWhiteSpace = false;
            _symbolChars = new char[] { '=', '+', '-', '/', ',', '.', '*', '~', '!', '@', '#', '$', '%', '^', '&', '(', ')', '{', '}', '[', ']', ':', ';', '<', '>', '?', '|', '\\' };

            _line = 1;
            _column = 1;
            _pos = 0;
            _previous = null;
        }

        private void StartRead()
        {
            _saveLine = _line;
            _saveCol = _column;
            _savePos = _pos;
        }

        private char LA(int count)
        {
            if (_pos + count >= _data.Length)
                return EOF;
            else
                return _data[_pos + count];
        }

        private char Consume()
        {
            char ret = _data[_pos];
            _pos++;
            _column++;

            return ret;
        }

        private Token CreateToken(TokenKind kind, string value)
        {
            return new Token(kind, value, _line, _column);
        }

        private Token CreateToken(TokenKind kind)
        {
            string tokenData = _data.Substring(_savePos, _pos - _savePos);
            return new Token(kind, tokenData, _saveLine, _saveCol);
        }

        private Token ReadWhitespace()
        {
            StartRead();

            Consume(); // consume the looked-ahead whitespace char

            while (true)
            {
                char ch = LA(0);
                if (ch == '\t' || ch == ' ')
                    Consume();
                else
                    break;
            }

            return CreateToken(TokenKind.WhiteSpace);

        }

        private Token ReadNumber()
        {
            StartRead();

            bool hadDot = false;

            Consume(); // read first digit

            while (true)
            {
                char ch = LA(0);
                if (Char.IsDigit(ch))
                    Consume();
                else if (ch == '.' && !hadDot)
                {
                    hadDot = true;
                    Consume();
                }
                else
                    break;
            }

            return CreateToken(TokenKind.Number);
        }

        private Token ReadWord()
        {
            StartRead();

            Consume(); // consume first character of the word

            while (true)
            {
                char ch = LA(0);
                if (Char.IsLetterOrDigit(ch) || ch == '_')
                    Consume();
                else
                    break;
            }

            return CreateToken(TokenKind.Word);
        }

        private Token ReadString()
        {
            StartRead();

            Consume(); // read "

            while (true)
            {
                char ch = LA(0);
                if (ch == EOF)
                    break;
                else if (ch == '\r')	// handle CR in strings
                {
                    Consume();
                    if (LA(0) == '\n')	// for DOS & windows
                        Consume();

                    _line++;
                    _column = 1;
                }
                else if (ch == '\n')	// new line in quoted string
                {
                    Consume();

                    _line++;
                    _column = 1;
                }
                else if (ch == _quoteChar)
                {
                    Consume();
                    if (LA(0) != _quoteChar)
                        break;	// done reading, and this quotes does not have escape character
                    else
                        Consume(); // consume second quote character, because first was just an escape
                }
                else
                    Consume();
            }

            return CreateToken(TokenKind.QuotedString);
        }

        private bool IsSymbol(char c)
        {
            for (int i = 0; i < _symbolChars.Length; i++)
                if (_symbolChars[i] == c)
                    return true;

            return false;
        }
        
        public StringTokenizer(TextReader reader)
        {
            if (reader == null)
                throw new ArgumentNullException("reader");

            _data = reader.ReadToEnd();

            Reset();
        }

        public StringTokenizer(string data)
        {
            if (data == null)
                throw new ArgumentNullException("data");

            _data = data;

            Reset();
        }

        public char[] SymbolChars
        {
            get
            {
                return _symbolChars;
            }
            set
            {
                _symbolChars = value;
            }
        }

        public char QuoteChar
        {
            get
            {
                return _quoteChar;
            }
            set
            {
                _quoteChar = value;
            }
        }

        public bool IgnoreWhiteSpace
        {
            get
            {
                return _ignoreWhiteSpace;
            }
            set
            {
                _ignoreWhiteSpace = value;
            }
        }

        public Token Peek()
        {
            int pos = _pos;
            int line = _line;
            int column = _column;
            
            Token result = Next();
            
            _pos = pos;
            _line = line;
            _column = column;

            return result;
        }

        public Token Next()
        {
            Token result;
            _previous = _current;
        ReadToken:

            char ch = LA(0);
            if (ch == _quoteChar)
                return ReadString();

            switch (ch)
            {
                case EOF:
                    result = CreateToken(TokenKind.EOF, string.Empty);
                    break;

                case ' ':
                case '\t':
                    {
                        if (_ignoreWhiteSpace)
                        {
                            Consume();
                            goto ReadToken;
                        }
                        else
                            result = ReadWhitespace();
                    }
                    break;
                
                case '0':
                case '1':
                case '2':
                case '3':
                case '4':
                case '5':
                case '6':
                case '7':
                case '8':
                case '9':
                    return ReadNumber();

                case '\r':
                    {
                        StartRead();
                        Consume();
                        if (LA(0) == '\n')
                            Consume();	// on DOS/Windows we have \r\n for new line

                        _line++;
                        _column = 1;

                        result = CreateToken(TokenKind.EOL);
                    }
                    break;

                case '\n':
                    {
                        StartRead();
                        Consume();
                        _line++;
                        _column = 1;

                        result = CreateToken(TokenKind.EOL);
                    }
                    break;

                default:
                    {
                        if (Char.IsLetter(ch) || ch == '_')
                            return ReadWord();
                        else if (IsSymbol(ch))
                        {
                            StartRead();
                            Consume();
                            result = CreateToken(TokenKind.Symbol);
                        }
                        else
                        {
                            StartRead();
                            Consume();
                            result = CreateToken(TokenKind.Unknown);
                        }
                    }
                    break;
            }

            _current = result;
            return result;
        }

        public Token Previous
        {
            get
            {
                return _previous;
            }
        }

        #region IEnumerable<Token> Members

        IEnumerator<Token> IEnumerable<Token>.GetEnumerator()
        {
            Token token = null;
            while (true)
            {
                token = Next();
                if (token.Kind == TokenKind.EOF)
                    yield break;
                else
                    yield return token;
            }
        }

        #endregion

        #region IEnumerable Members

        IEnumerator IEnumerable.GetEnumerator()
        {
            Token token = null;
            while (true)
            {
                token = Next();
                if (token.Kind == TokenKind.EOF)
                    yield break;
                else
                    yield return token;
            }
        }

        #endregion
    }

    public enum TokenKind
    {
        Unknown,
        Word,
        Number,
        QuotedString,
        WhiteSpace,
        Symbol,
        EOL,
        EOF
    }

    public class Token
    {
        private int _line;
        private int _column;
        private string _value;
        private TokenKind _kind;

        internal Token(TokenKind kind, string value, int line, int column)
        {
            _kind = kind;
            _value = value;
            _line = line;
            _column = column;
        }

        public int Column
        {
            get
            {
                return _column;
            }
        }

        public TokenKind Kind
        {
            get
            {
                return _kind;
            }
        }

        public int Line
        {
            get
            {
                return _line;
            }
        }

        public string Value
        {
            get
            {
                return _value;
            }
        }
    }
}
